Skip to content

refactor(architecture): enforce module boundaries - #153

Merged
ClaudiaFang merged 5 commits into
1.6.1from
refactor/1.6.1-architecture-boundaries
Sep 1, 2026
Merged

refactor(architecture): enforce module boundaries#153
ClaudiaFang merged 5 commits into
1.6.1from
refactor/1.6.1-architecture-boundaries

Conversation

@ClaudiaFang

Copy link
Copy Markdown
Member

Summary

1.6.1 already carries docs/architecture.md and docs/bug-fix-guidelines.md (the MUST/MUST NOT module-boundary contract), but the code hadn't fully caught up to it yet, and CLAUDE.md still described the pre-refactor shape. This PR makes the code satisfy that contract before more bug fixes land and drift further from it — it is not a general cleanup pass. Scope is capped to what's listed below; see "Explicitly out of scope" for what's deliberately deferred.

  • main.ts as a real composition/lifecycle root — extracted src/runtime/createSyncRuntime.ts to own the sync-domain + Source Control application constructor graph (SyncManager, SyncStatusRefreshService, SyncDiffService, SyncWorkspace, ChangeRepository, SyncSelectionStore, OperationState, RefreshState, SourceControlViewModel, SourceControlActionService). main.ts now only knows Obsidian lifecycle: settings load/save, view/command/ribbon registration, vault event registration.
  • Split SyncStatusRefreshService (was 661 lines, three algorithms in one class) into SyncFileDiscovery, SyncStatusResolver, and RenameReconciler, each with a single owning responsibility. SyncStatusRefreshService now only orchestrates discovery → resolve → reconcile → publish, plus the incremental create/modify/delete/rename handlers (deliberately not split further — see scope below).
  • SourceControlView low-risk extraction — pulled the "Sync Queue" and "Repository Changes" regions into SyncQueueSection/RepositoryChangesSection, matching the existing FilterMenu/SourceControlHeader pattern (pure functions: state + callbacks in, never SyncWorkspace/SourceControlActionService/SourceControlViewModel directly).
  • SyncWorkspace boundary auditgit grep across src/ui/** and src/logic/source-control/** found the boundary already intact (no UI code touches SyncWorkspace/coordinators directly; the one PushCoordinator hit is a pre-existing type-only import). No code fix needed for the UI → sync direction.
  • ESLint boundary guards (eslint.config.mts) — new no-restricted-imports rules: src/ui/** and src/logic/source-control/** may not import a concrete Git provider or a push/pull coordinator/executor directly; src/logic/sync/** may not import src/ui/source-control/**.
  • Real violation caught by the new rule — the last rule surfaced a genuine pre-existing reverse dependency: SyncDiffService/SyncInteractionPort (sync domain) imported computeDiffStat/DiffStatLoadResult from ui/source-control. Moved the pure diff-stat computation and the DiffStatLoadResult contract into a new src/logic/sync/DiffStat.ts; the UI now depends on the domain for these, not the reverse.
  • Tests — new SyncFileDiscovery.test.ts, SyncStatusResolver.test.ts, RenameReconciler.test.ts, createSyncRuntime.test.ts, DiffStat.test.ts (net-new coverage — the old SyncStatusRefreshService.test.ts only covered incremental handlers, not discovery/resolution/rename-reconciliation directly).
  • DocsCLAUDE.md's "Code Architecture" section replaced with a short contract pointing at docs/architecture.md/docs/bug-fix-guidelines.md (also fixed stale GitLab/GitHub-only provider list → GitHub/GitLab/Gitea, and the single-file sync-manager.ts description). docs/architecture.md's module table and "Current hotspots" section updated to describe the merged code.

Explicitly out of scope (per plan)

No PushCoordinator/PullCoordinator split, no SyncManager rewrite, no provider/BaseGitService cleanup, no settings restructure, no ChangeItem rewrite, no CSS/UX changes, no sync semantics changes, no new features, no version bump, no DI framework, no command/event bus, no create/modify/delete/rename per-event handler classes.

Verification

  • npx eslint . — 0 errors (verified after every commit, including the new boundary-guard rules actually firing on the real violation above before the fix).
  • npx vitest run — 74 files / 933 tests passed (up from 73 files / 917 tests on 1.6.1; net-new coverage, no regressions).
  • npm run build (tsc -noEmit + Obsidian 1.11.0 compat typecheck + esbuild production) — passed after every commit.
  • Boundary re-audit after the split: git grep -n "SyncWorkspace" -- src/ui → only doc-comment mentions. git grep -n "PushCoordinator\|PullCoordinator" -- src/ui src/logic/source-control → only the pre-existing type-only import.

DoD checklist

  • CLAUDE.md 與 docs/architecture.md 一致
  • main.ts 不再組裝整個 Source Control/Sync constructor graph
  • SyncStatusRefreshService 不再自己實作 discovery algorithm
  • status classification 有明確 owner (SyncStatusResolver)
  • rename reconciliation 有明確 owner (RenameReconciler)
  • Source Control execution 全部經 SyncWorkspace(audit:本來就成立)
  • UI 沒有 concrete provider dependency
  • sync domain 沒有 Source Control UI dependency(新規則抓到並修正了既有違規)
  • ESLint 能阻止上述 dependency regression
  • SourceControlView 明顯降低責任密度
  • existing sync semantics 不變
  • unit tests green
  • build/lint green
  • GitHub/GitLab/Gitea E2E green — not run this session (needs provisioned credentials); no sync semantics changed, so not expected to be affected, but flagging as unverified.

🤖 Generated with Claude Code

ClaudiaFang and others added 5 commits September 1, 2026 04:42
…r/reconciler

Give file discovery, status resolution, and rename reconciliation each a
single owning class (SyncFileDiscovery, SyncStatusResolver,
RenameReconciler) instead of one 661-line service implementing all three
algorithms. SyncStatusRefreshService now only orchestrates the three plus
the incremental create/modify/delete/rename handlers, per the module
boundaries in docs/architecture.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ol composition root

Move the sync-domain and Source Control application constructor graph
(SyncManager, SyncStatusRefreshService, SyncDiffService, SyncWorkspace,
ChangeRepository, SyncSelectionStore, OperationState, RefreshState,
SourceControlViewModel, SourceControlActionService, and the
ChangeRepository<->SyncStatusService wiring) out of main.ts and into
src/runtime/createSyncRuntime.ts. main.ts now only knows Obsidian
lifecycle: settings load/save, view/command/ribbon registration, vault
event registration -- it no longer needs to know the sync/Source Control
constructor graph to add a plugin lifecycle hook.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rom SourceControlView

Pull the "Sync Queue" and "Repository Changes" regions out of the 730-line
SourceControlView into standalone render functions, matching the existing
FilterMenu/SourceControlHeader/ChangeTree pattern: pure functions taking
state + callbacks, never SyncWorkspace/SourceControlActionService/
SourceControlViewModel directly. SourceControlView keeps ownership of view
state (collapsed sections, view mode, folder collapse) and now only
orchestrates rendering, the diff pane, and scroll-state management.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sync->UI dependency

Add no-restricted-imports rules per docs/architecture.md: src/ui/** and
src/logic/source-control/** may not import a concrete Git provider or a
push/pull coordinator/executor directly (must go through SyncWorkspace),
and src/logic/sync/** may not import src/ui/source-control/** (dependency
direction runs UI -> domain, never back).

The last rule caught a real pre-existing violation: SyncDiffService and
SyncInteractionPort (sync domain) imported computeDiffStat and
DiffStatLoadResult from ui/source-control. Move the pure diff-stat
computation (computeDiffStat, cheapLocalStat, addedContentStat,
deletedContentStat) and the DiffStatLoadResult contract into a new
src/logic/sync/DiffStat.ts; ChangePresentation.ts and DiffStatProvider.ts
now depend on the domain for these instead of the other way around.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ndaries

CLAUDE.md's "Code Architecture" section duplicated (and had drifted from)
docs/architecture.md: it said GitLab/GitHub only (no Gitea, though
GiteaService already existed) and described sync-manager.ts as a single
file. Replace it with a short contract pointing at docs/architecture.md
(and docs/bug-fix-guidelines.md for bug fixes) plus the two compatibility
gotchas that aren't covered there.

docs/architecture.md's module table and "Current hotspots" section now
describe the merged code: createSyncRuntime as the composition root,
SyncFileDiscovery/SyncStatusResolver/RenameReconciler as
SyncStatusRefreshService's three collaborators, DiffStat.ts, and
SourceControlView's reduced scope after the SyncQueueSection/
RepositoryChangesSection extraction.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

@ClaudiaFang
ClaudiaFang merged commit 69e5540 into 1.6.1 Sep 1, 2026
10 of 20 checks passed
@ClaudiaFang
ClaudiaFang deleted the refactor/1.6.1-architecture-boundaries branch September 1, 2026 05:10
@ClaudiaFang

Copy link
Copy Markdown
Member Author

🎉 This PR is included in version 1.6.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant